Skip to content

fix(rewrite): prevent walrus operator double evaluation in assertions - #14447

Open
RonnyPfannschmidt wants to merge 9 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:ronny/fix-14445-assert-walrus
Open

fix(rewrite): prevent walrus operator double evaluation in assertions#14447
RonnyPfannschmidt wants to merge 9 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:ronny/fix-14445-assert-walrus

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #14445 — assertion rewriting evaluated walrus operator (:=) expressions
multiple times, so an assertion over an expression with side effects reported a
result the test never actually computed.

Root cause: the variables_overwrite mechanism stored NamedExpr AST nodes
and substituted them back into every later read of the target name — in
subsequent assertions, in _call_reprcompare's results tuple, and in
explanation formatting. Each substitution is another evaluation.

Fix: remove variables_overwrite entirely, and instead

  • keep walrus expressions in their natural evaluation position, so left-to-right
    order is the interpreter's, not the rewriter's;
  • reference the target variable in explanations rather than re-evaluating it;
  • freeze conflicting left operands via assign() when a later walrus targets the
    same name, so the explanation reports what that operand actually saw;
  • capture BoolOp conditions in stable temps for the explanation path.

The self.scope / _SCOPE_END_MARKER bookkeeping goes with it: it existed only
to key variables_overwrite per scope.

What is in the diff

Four files, ~160 lines of rewriter change:

file
src/_pytest/assertion/rewrite.py the fix
testing/test_assertrewrite_coverage.py +15 matrix cases, all passing
testing/test_assertrewrite.py net shrink, see below
changelog/14445.bugfix.rst

Coverage

The new cases in the matrix cover single evaluation (compare, boolop,
chained compare) and evaluation order — that an operand evaluated before a
later walrus is reported with its pre-assignment value, for compare left
operands, call arguments (positional, keyword and **), BinOp left operands and
chained-compare operands.

The three evaluation-order cases @scapalive reported are among them. They are
not regressions from this PR — they fail on main too — but they are the same
defect, so they are fixed and covered here rather than deferred.

Test cull

TestAssertionRewriteWalrusOperator is gone. Nine of its twelve runpytest()
tests are answered more precisely by the matrix, two moved there (one as
introspection, one as evaluation order), and one stayed in
test_assertrewrite.py because it needs two tests in one module to say
anything. TestIssue14445 keeps the two reproducers from the issue itself.

The composite-chain case is the reason this is a cull and not just tidying: it
passes on main, and it passes because of the bug. main rewrites the walrus
target to an internal temp and substitutes the stored NamedExpr into every
later read of the name, including the next statement — so the assert a is None
meant to verify the outcome re-runs the walrus and produces the value it is
checking for. Asked through returned values instead of ret == 0, main fails
that case.

Review notes

@bluetech — the BoolOp explanation you flagged is fixed on this branch. On

def side_effect():
    return True

def test_walrus_boolop():
    assert (x := side_effect()) and (x := False)

main reports assert (False and False); this branch reports

E       assert (True and False)

which is the case now pinned by test_walrus_in_boolop_reports_each_operand.

The AST before/after you asked for is what scripts/assert_rewrite_diff.py
prints — that script landed separately in #14921, and #14921 shows its output
for exactly this snippet on main.

Status

Rebased onto main. #14813 (the coverage matrix) and #14921 (the rewrite-diff
script) have both merged, so this branch no longer carries either; earlier
revisions of this description referred to them as pending.

Full suite green on the rebased branch: 4511 passed, 46 skipped, 17 xfailed.


AI-assisted, reviewed and driven by me; see the Co-authored-by trailers on the
commits.

Copilot AI review requested due to automatic review settings May 8, 2026 08:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes assertion rewriting so walrus (:=) expressions are not evaluated multiple times, preventing side effects from running twice and producing incorrect rewritten-assert behavior (per #14445).

Changes:

  • Removes the prior variables_overwrite/scope-tracking mechanism and adjusts NamedExpr handling to avoid re-evaluation in explanations.
  • Updates BoolOp/Compare rewriting to stabilize conditions/operands for explanation formatting.
  • Adds new regression tests for walrus side-effect/double-evaluation cases and updates existing expected assertion output.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/_pytest/assertion/rewrite.py Refactors assertion-rewrite AST generation around NamedExpr, BoolOp, and Compare to avoid walrus re-evaluation.
testing/test_assertrewrite.py Updates expected assertion output and adds regression tests for #14445 scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/_pytest/assertion/rewrite.py Outdated
Comment thread src/_pytest/assertion/rewrite.py Outdated
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label May 8, 2026
@RonnyPfannschmidt RonnyPfannschmidt mentioned this pull request May 9, 2026
7 tasks

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but we probably want another person to look at it

@bluetech bluetech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't reviewed the code yet, before that I dumped the rewritten AST before/after this PR, on the following example:

def side_effect():
    return True

def test_walrus_boolop():
    assert (x := side_effect())
Before
Module(
    body=[
        Import(
            names=[
                alias(name='builtins', asname='@py_builtins')]),
        Import(
            names=[
                alias(name='_pytest.assertion.rewrite', asname='@pytest_ar')]),
        FunctionDef(
            name='side_effect',
            args=arguments(),
            body=[
                Return(
                    value=Constant(value=True))]),
        FunctionDef(
            name='test_walrus_boolop',
            args=arguments(),
            body=[
                If(
                    test=UnaryOp(
                        op=Not(),
                        operand=NamedExpr(
                            target=Name(id='x', ctx=Store()),
                            value=Call(
                                func=Name(id='side_effect', ctx=Load())))),
                    body=[
                        Assign(
                            targets=[
                                Name(id='@py_format1', ctx=Store())],
                            value=BinOp(
                                left=BinOp(
                                    left=Constant(value=''),
                                    op=Add(),
                                    right=Constant(value='assert %(py0)s')),
                                op=Mod(),
                                right=Dict(
                                    keys=[
                                        Constant(value='py0')],
                                    values=[
                                        IfExp(
                                            test=BoolOp(
                                                op=Or(),
                                                values=[
                                                    Compare(
                                                        left=Constant(value='x'),
                                                        ops=[
                                                            In()],
                                                        comparators=[
                                                            Call(
                                                                func=Attribute(
                                                                    value=Name(id='@py_builtins', ctx=Load()),
                                                                    attr='locals',
                                                                    ctx=Load()))]),
                                                    Call(
                                                        func=Attribute(
                                                            value=Name(id='@pytest_ar', ctx=Load()),
                                                            attr='_should_repr_global_name',
                                                            ctx=Load()),
                                                        args=[
                                                            NamedExpr(
                                                                target=Name(id='x', ctx=Store()),
                                                                value=Call(
                                                                    func=Name(id='side_effect', ctx=Load())))])]),
                                            body=Call(
                                                func=Attribute(
                                                    value=Name(id='@pytest_ar', ctx=Load()),
                                                    attr='_saferepr',
                                                    ctx=Load()),
                                                args=[
                                                    NamedExpr(
                                                        target=Name(id='x', ctx=Store()),
                                                        value=Call(
                                                            func=Name(id='side_effect', ctx=Load())))]),
                                            orelse=Constant(value='x'))]))),
                        Raise(
                            exc=Call(
                                func=Name(id='AssertionError', ctx=Load()),
                                args=[
                                    Call(
                                        func=Attribute(
                                            value=Name(id='@pytest_ar', ctx=Load()),
                                            attr='_format_explanation',
                                            ctx=Load()),
                                        args=[
                                            Name(id='@py_format1', ctx=Load())])]))])])])
After
Module(
    body=[
        Import(
            names=[
                alias(name='builtins', asname='@py_builtins')]),
        Import(
            names=[
                alias(name='_pytest.assertion.rewrite', asname='@pytest_ar')]),
        FunctionDef(
            name='side_effect',
            args=arguments(),
            body=[
                Return(
                    value=Constant(value=True))]),
        FunctionDef(
            name='test_walrus_boolop',
            args=arguments(),
            body=[
                If(
                    test=UnaryOp(
                        op=Not(),
                        operand=NamedExpr(
                            target=Name(id='x', ctx=Store()),
                            value=Call(
                                func=Name(id='side_effect', ctx=Load())))),
                    body=[
                        Assign(
                            targets=[
                                Name(id='@py_format1', ctx=Store())],
                            value=BinOp(
                                left=BinOp(
                                    left=Constant(value=''),
                                    op=Add(),
                                    right=Constant(value='assert %(py0)s')),
                                op=Mod(),
                                right=Dict(
                                    keys=[
                                        Constant(value='py0')],
                                    values=[
                                        IfExp(
                                            test=BoolOp(
                                                op=Or(),
                                                values=[
                                                    Compare(
                                                        left=Constant(value='x'),
                                                        ops=[
                                                            In()],
                                                        comparators=[
                                                            Call(
                                                                func=Attribute(
                                                                    value=Name(id='@py_builtins', ctx=Load()),
                                                                    attr='locals',
                                                                    ctx=Load()))]),
                                                    Call(
                                                        func=Attribute(
                                                            value=Name(id='@pytest_ar', ctx=Load()),
                                                            attr='_should_repr_global_name',
                                                            ctx=Load()),
                                                        args=[
                                                            Name(id='x', ctx=Load())])]),
                                            body=Call(
                                                func=Attribute(
                                                    value=Name(id='@pytest_ar', ctx=Load()),
                                                    attr='_saferepr',
                                                    ctx=Load()),
                                                args=[
                                                    Name(id='x', ctx=Load())]),
                                            orelse=Constant(value='x'))]))),
                        Raise(
                            exc=Call(
                                func=Name(id='AssertionError', ctx=Load()),
                                args=[
                                    Call(
                                        func=Attribute(
                                            value=Name(id='@pytest_ar', ctx=Load()),
                                            attr='_format_explanation',
                                            ctx=Load()),
                                        args=[
                                            Name(id='@py_format1', ctx=Load())])]))])])])

The diff is:

@@ -57,20 +57,14 @@
                                                             attr='_should_repr_global_name',
                                                             ctx=Load()),
                                                         args=[
-                                                            NamedExpr(
-                                                                target=Name(id='x', ctx=Store()),
-                                                                value=Call(
-                                                                    func=Name(id='side_effect', ctx=Load())))])]),
+                                                            Name(id='x', ctx=Load())])]),
                                             body=Call(
                                                 func=Attribute(
                                                     value=Name(id='@pytest_ar', ctx=Load()),
                                                     attr='_saferepr',
                                                     ctx=Load()),
                                                 args=[
-                                                    NamedExpr(
-                                                        target=Name(id='x', ctx=Store()),
-                                                        value=Call(
-                                                            func=Name(id='side_effect', ctx=Load())))]),
+                                                    Name(id='x', ctx=Load())]),
                                             orelse=Constant(value='x'))]))),
                         Raise(
                             exc=Call(

This looks good for the issue, since now we no longer run side_effect twice three times.

However if I tweak in this way:

def side_effect():
    return True

def test_walrus_boolop():
    assert (x := side_effect()) and (x := False)

the assertion is

x.py:5: in test_walrus_boolop
    assert (x := side_effect()) and (x := False)
E   assert (False and False)

which is incorrect (should be assert (True and False)). That said, this also happens in main.

Let me know if you want to tackle this problem in this PR as well, in which I'll wait before reviewing, or if I should open a separate issue for that and review this PR as is.

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

good find, i'll address it in here

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

i found a interesting issue about very duplicate tracking, investigating now

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

now the change is a litte bigger than intended

@bluetech bluetech added backport 9.1.x apply to PRs at any point; backports the changes to the 9.1.x branch and removed backport 9.0.x labels Jun 14, 2026
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the ronny/fix-14445-assert-walrus branch 2 times, most recently from acd9277 to c4369d0 Compare July 21, 2026 08:55
@scapalive

Copy link
Copy Markdown

Thanks for working on this. A differential check found three remaining
evaluation-order cases on PR HEAD
c4369d07873f5b7032f9e9baa4d405cc6ea4aad1.

def identity(value):
    return value


def test_compare_preserves_pre_walrus_left_value():
    value = "Hello"
    assert value != identity(value := value.lower())
    assert value == "hello"

Python evaluates the left operand before the call, so this compares
"Hello" != "hello" and passes. With assertion rewriting at the tested PR
HEAD, it compares "hello" != "hello" and fails.

def collect(*values):
    return values


def test_call_preserves_earlier_positional_argument():
    value = "Hello"
    assert collect(value, identity(value := value.lower())) == (
        "Hello",
        "hello",
    )
    assert value == "hello"

The first positional argument should already be "Hello" when the second
argument assigns "hello". Rewriting instead passes ("hello", "hello") to
collect.

A related false-comparison case is:

def test_failed_compare_uses_pre_walrus_left_value():
    value = 2
    try:
        assert value == identity(value := 3)
    except AssertionError:
        pass
    else:
        raise AssertionError("assertion was rewritten as 3 == 3")
    assert value == 3

This must compare 2 == 3 and raise AssertionError; at the tested PR HEAD
the inner assertion unexpectedly passes.

Values evaluated before a later walrus need to be captured before that
assignment changes the target. I ran the three tests twice, using isolated
source paths to avoid rewritten-bytecode reuse:

Environment Result (both runs)
Direct Python 3.13.13 3 passed
pytest --assert=plain 3 passed
upstream base 56b196e921acec0259d84622a570fde6032e15b5 3 failed
independent corrected implementation 3 passed
this PR c4369d07873f5b7032f9e9baa4d405cc6ea4aad1 3 failed

I can provide tests adapted to testing/test_assertrewrite.py if useful.

These cases were identified with automated assistance, then reduced and
verified by differential execution on normal Python, --assert=plain,
upstream base, and this PR. Denis Scapin reviewed the reproductions and takes
responsibility for this report.

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

Rebased and extended. Two changes worth calling out.

The reported evaluation-order cases are fixed here. @scapalive — thank you, all three reproduce and all three are now covered. They are not regressions from this PR (they fail on main too, as your table says), but they are the same defect class it claims to close, so they belong here rather than in a follow-up.

The mechanism was already in the PR, just too shallow, and inconsistently so between two visitors this PR touches: visit_BoolOp did a deep ast.walk pre-scan for later walrus targets, visit_Compare structurally matched only a comparator that was an ast.NamedExpr (so a walrus nested inside a call slipped past), and visit_Call had no guard at all. That pre-scan is now _walrus_targets() plus a visit_operand() helper, used by visit_BoolOp, visit_Compare, visit_Call and visit_BinOp.

This now sits on #14813, a test-only PR that adds a coverage matrix for the rewriter and records every known gap as a strict xfail. This PR closes four of those groups — single-eval-walrus, order-compare-left, order-call-argument, order-binop-left — taking the count from 24 to 16, and the strict markers mean it cannot close one without saying so. Your three cases live there as the order-* entries; the systematic form of them is more useful in that matrix than as one-off regression tests, so TestIssue14445 keeps only the reproducers from the issue itself.

One order-call-argument entry survives this PR: a bare walrus argument is still substituted into a later one, which visit_operand() does not see because the operand is a NamedExpr rather than a Name. That is the next PR in the stack.

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see anything shocking by skimming. i'll review in details later.

Maybe the helper script to compare two pytest versions' output could be in their own PR ? In pylint and mypy there is a primer that permits to see the change in output in a feature branch compared to main by running pylint/mypy on a choice selection of open source repos. Maybe it could be adapted for pytest based on those two scripts.

Also the big file with separator as comment could be burst into a directory of multiple files?

@bluetech

Copy link
Copy Markdown
Member

It would be nice to see original/rewritten AST for some simple case, to get a quick sense of the new method, before diving into the code. If the AST can also be written in source code form it would make it even easier. Maybe for the example given above:

def side_effect():
    return True

def test_walrus_boolop():
    assert (x := side_effect()) and (x := False)

Sorry I'm too lazy to do it myself...

assert result.ret == 0


class TestIssue14445:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these tests redundant with the coverage ones, or are they testing something separate?

Also in #14813 (comment) you said you'll delete TestAssertionRewriteWalrusOperator here. Is it not redundant now?

@Pierre-Sassoulas

Copy link
Copy Markdown
Member

To bluetech comment, I made a pytest plugin to do golden master / caracterisation tests (pytest-remaster) which is what we want to do here for easy review and update of ast changes imo. Let me know what you think.

@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the ronny/fix-14445-assert-walrus branch 3 times, most recently from b3f3947 to 0253180 Compare August 26, 2026 10:04
RonnyPfannschmidt and others added 9 commits August 30, 2026 22:32
Fixes pytest-dev#14445 - assertion rewriting evaluated NamedExpr (:=) expressions
multiple times, causing side effects to fire repeatedly.

The root cause was the `variables_overwrite` mechanism which stored and
re-evaluated NamedExpr AST nodes in subsequent assertions, in
`_call_reprcompare`'s results tuple, and in explanation formatting.

The fix:
- visit_NamedExpr: reference the target variable in explanations instead
  of re-evaluating the full expression
- visit_Compare: assign left-side NamedExpr to a temp before right-side
  hoisting; freeze left_res when a comparator walrus targets the same
  name; replace NamedExpr entries in `results` with target variables
- visit_BoolOp: capture short-circuit condition in a stable temp for the
  explanation path; remove walrus target rename logic
- visit_Call: remove variables_overwrite substitution (walrus now properly
  assigns to user variables in its natural evaluation position)
- Remove variables_overwrite, scope tracking, Sentinel class

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Add tests for two remaining walrus double-evaluation scenarios:
- Bare NamedExpr as BoolOp operand evaluated twice via condition check
- Same walrus target in chained comparison evaluated multiple times

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Use the already-assigned res_var to build the short-circuit condition
instead of the raw visitor result, preventing bare NamedExpr operands
from being evaluated a second time when checking truthiness.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
In a chained comparison like `(x := f()) < (x := g()) < (x := h())`,
each NamedExpr comparator is now assigned to a temp variable so it
evaluates exactly once. Previously the raw NamedExpr node would be
reused as left_res in the next iteration, causing double evaluation.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
When multiple walrus operators target the same variable in a BoolOp
(e.g., `assert (x := side_effect()) and (x := False)`), the assertion
explanation previously showed the final value of `x` for all operands
because the format context evaluated lazily after all operands ran.

Fix by tracking Name/NamedExpr operand values in stable @py_assert
variables (via self.assign) immediately after evaluation, then pointing
the explanation format context at the tracked copy. This uses the same
value-tracking mechanism already used by visit_Call, visit_Attribute, etc.

Fixes the case reported by @bluetech in PR review.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Replace the blanket snapshot-all-operands approach with a targeted one:
pre-scan the BoolOp to find walrus targets, then only snapshot operands
whose value a later walrus would corrupt.

Snapshot rules:
- NamedExpr (non-last): always, to avoid re-evaluating side effects
- Name with later walrus conflict: to freeze the pre-overwrite value
- Everything else: use res directly (stable @py_assert or plain name)

Non-walrus BoolOps now generate identical code to 8.3.5 (no snapshots).

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
The rewriter hoists each operand into its own statement, but a plain
name is left as a bare load evaluated when the enclosing expression is
assembled -- after the statements of the operands that follow it.  A
walrus operator in a later operand rebinds the name in between, so both
the value used and the value reported were the post-walrus one, while
Python evaluates the earlier operand first:

    assert value != identity(value := value.lower())

visit_BoolOp already guarded against this; extract its pre-scan as
_walrus_targets() and add visit_operand() to apply the same freeze in
visit_Compare, visit_Call and visit_BinOp.  visit_Compare previously
matched only a comparator that *was* a NamedExpr, missing walrus
operators nested inside it; visit_Call did not guard at all, so an
earlier argument saw a later argument's assignment.

These cases predate the walrus rework -- they fail on main too.

Closes the single-eval-walrus, order-compare-left, order-call-argument
and order-binop-left groups in the coverage matrix.  order-call-argument
keeps one entry: a bare walrus argument is still substituted into a
later one, which visit_operand does not yet see because the operand is a
NamedExpr rather than a Name.

Reported-by: Denis Scapin
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TestAssertionRewriteWalrusOperator predates the coverage matrix and asks
its questions through runpytest(): twelve tests that mostly check ret == 0.
Nine of them the matrix already answers in-process and more precisely --
what the failure message says, what the operands saw, how often each ran.

Two are worth more than a deletion, so they move rather than vanish:

  assert not (a and ((a := False) is False))

reads back as an introspection case, and the composite chain

  assert a and True and ((a := False) is False) and (a is False) and ...

as an evaluation-order one.  That second is why the deletion is not just
tidying: it passes on main, and it passes there because of the bug.  Main
rewrites the walrus target to an internal temp and substitutes the stored
NamedExpr into every later read of the name -- including the next
statement, so the `assert a is None` that is supposed to verify the outcome
re-runs the walrus and creates it.  Asked through returned values instead
of ret == 0, main fails the case.

TestIssue14445 loses the four tests that restate matrix single-evaluation
entries this PR already adds, and keeps the two reproducers from the issue.

One test survives in place: nothing the rewriter stores may outlive a
statement, which needs two tests in one module to observe and so cannot be
said in-process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The core rewrite changes are well-covered by targeted tests, with only a minor changelog wording nit noted.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

@@ -0,0 +1,3 @@
Fixed assertion rewriting evaluating walrus operator (``:=``) expressions multiple times, causing incorrect test results when the expression had side effects (e.g., incrementing a counter or calling a function).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport 9.1.x apply to PRs at any point; backports the changes to the 9.1.x branch bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Walrus expression duplicate evaluation failures with rewrite

5 participants